Linguaggio C

 

per collaborazioni, commenti, critiche, e altro contattateci alla e-mail: clubinfo@libero.it risponderemo al più presto!

Le variabili

di Luca Sabatucci

Lezione 4

Pagina principale | Lezione precedente | Lezione successiva


Dichiarazione e riferimento

Una delle caratteristiche salienti di un linguaggio di programmazione è costituita dai tipi di dato che esso permette di rappresentare o direttamente o mediante definizioni dell'utente. I computer sono basati sul concetto di una memoria principale formata da celle elementari, ognuna identificata da un indirizzo di memoria. Il contenuto di tali celle rappresenta il suo valore. Il valore delle celle può essere letto e modificato. La variabile è quindi una astrazione del contenuto di tali celle di memoria.
Una variabile è caratterizzata da un nome e da quattro attributi: scopo, durata, valore e tipo.

Nomi ed etichette

I nomi delle variabili, delle funzioni, delle etichette e dei tipi definiti dall'utente, sono chiamati identificatori e seguono delle regole lessicali ben definite.

Ecco ad esempio alcuni identificatori corretti:


raggio
_area_del_cerchio
contatore2

questi identificatori invece non sono corretti:

	
raggio?
area..cerchio
2contatore

Il C standard ANSI stabilisce che gli identificatori possono essere di qualsiasi lunghezza, inoltre le lettere maiuscole e minuscole vengono considerate diverse. Un identificatore non può coincidere con una parola chiave del linguaggio.

Un consiglio importante è quello di fornire sempre dei nomi significativi alle variabili, che aiutino a far chiarezza sull'uso che se ne fa.
Ad esempio non è molto utile chiamare una variabile r; un nome più significativo - come raggio - già spiega qual'è il contenuto della variabile.
Esistono poi delle variabile che in genere si indicano sempre con lo stesso nome, ad esempio i contatori nei cicli (che io indico sempre con i,j,count,k,z...), e che semplificano lo sforzo di trovare un nome significativo.

Alcuni programmatori usano una notazione ancora più complessa per fornire un nome alle variabile. Fanno precedere tale nome da un suffisso che indica il tipo della variabile. Ad esempio se il raggio è una variabile di tipo double usano il nome d_raggio o _d_raggio.
A mio avviso questo tipo di notazione non dà grandi vantaggi in fase di programmazione, rispetto al peggioramento della leggibilità del listato. Per questo motivo in questo corso non farò uso di tale notazione.

Tipi primitivi

In C sono cinque i tipi di dati principali:

  1. char - caratteri
  2. int - numeri interi
  3. float - numeri in virgola mobile
  4. double - numeri in virgola mobile a doppia precisione
  5. void - non valori

E' poi possibile costruire tipi più complessi, ma nel farlo si usano sempre quelli principali come punto di partenza.

Si noti come il C non definisca tipi ad alto livello presenti in altri linguaggi come le stringhe o gli insiemi o i file.
Questi tipi di dati devono essere definiti direttamente dall'utente. Vedremo in seguito come.

Si noti inoltre la presenza di un tipo di dato molto particolare: il void. Questo viene generalmente usato per indicare che una funzione non accetta parametri o non restituisce valori.

Modificatori per tipi

Le specifiche del linguaggio non dicono nulla sulla dimensione in byte dei tipi primitivi. Ad esempio un char può occupare 1 o 2 byte a seconda delle implementazioni. ne consegue che non è possibile stabilire a priori tale dimensione. Questa libertà è stata lasciata per permettere l'implementazione dei compilatori su diversi tipi di macchina. Se si vuole quindi creare un programma portabile su più piattaforme non bisogna basare il codice su tale conoscenza.

Per andare incontro a diverse esigenze di lunghezza dei dati, ecco che sono stati introdotti dei modificatori per i tipi primitivi:

  1. signed
  2. unsigned
  3. long
  4. short

Questi modificatori si applicano prima del tipo della variabile per creare un nuovo tipo con determinate caratteristiche. I modificatori possono anche essere combinati tra loro.

I modificatori signed e unsigned specificano se la codifica del valore deve essere in valore assoluto oppure deve essere assegnato un bit per il segno.
I modificatori long e short creano una versione più lunga o più corta del tipo standard.

Ecco un elenco completo dei tipi, in ordine crescente di occupazione di memoria:

  1. char
  2. unsigned char
  3. signed char
  4. short int (o semplicemente short)
  5. unsigned short int
  6. signed short int
  7. int
  8. signed int
  9. unsigned int
  10. long int (o semplicemente long)
  11. signed long int
  12. unsigned long int
  13. float
  14. double
  15. long double

L'uso del modificatore signed segli int è ridondante perché la dichiarazione standard di questo tipo di dato prevede già il segno.
Invece la dichiarazione standard del tipo char è senza segno.

Dichiarazione delle variabili

Per essere usata una variabile deve prima essere dichiarata.
La dichiarazione deve necessariamente avvenire prima delle istruzioni di codice. Ci sono tre posizioni valide in cui è possibile dichiarare una variabile: all'interno di un blocco di istruzioni, tra i parametri di una funzione oppure all'esterno di tutte le funzioni. E' buona regola quando si dichiara una variabile di farla seguire da un breve ma significativo commento che chiarisce l'utilizzo che se ne fa.

Per dichiarare una variabile bisogna specificarne il tipo:


double raggio;
int cont;
char nome;

In C tutte le istruzioni devono essere terminate da un punto e virgola, compreso la dichiarazione. Inoltre è possibile dichiarare più variabili di uno stesso tipo elencandole con l'operatore virgola.
Ad esempio le righe:


double raggio;
double area;

possono essere sostituito da:


double raggio,area;

Quando viene dichiarata una variabile, le specifiche del linguaggio non forniscono alcuna garanzia sul suo contenuto. La variabile cioè non viene pulita e può contenere un valore totalmente imprevedibile. Per questo motivo nasce l'esigenza di assegnare sempre un valore ad una variabile prima di utilizzarla. Il C permette di inizializzare la variabile anche in fase di dichiarazione:


double raggio = 2.0,area = 0.0;
int cont = 0;
char nome = 'a';

La posizione in cui si dichiara la variabile ne definisce lo scopo, cioè se è una variabile locale o globale.

Variabili locali

Le variabili dichiarate all'interno di un blocco di istruzioni o tra i parametri formali di una funzione è una variabile locale. Lo scopo di una variabile locale è nel primo caso il blocco di istruzioni, nel secondo il blocco di istruzioni corrispondente all'intera funzione.
Una variabile definita in un blocco di istruzioni può essere riferita e usata solo all'interno di tale blocco. La vita di tali variabili è legata allo scopo: il controllo passa al blocco in questione viene allocato lo spazio in memoria per tutte le variabili locali ivi definite, e tale spazio viene rilasciato quando il controllo esce dal blocco.

Chiariamo il concetto di blocco di istruzioni. In C in qualunque posizione sia prevista una istruzione, è possibile mettere un blocco di istruzioni. Ad esempio:


if (a > b)
	istruzione;

l'istruzione può essere sostituita da un blocco. Un blocco inizia e termina con le parentesi graffe:


if (a > b) {
	c = d;
	m = n;
	...
}	

Una stessa funzione viene definita mediante parentesi graffe aperte e chiuse che ne identificano inizio e fine.


int calcola_area_cerchio(int raggio) {
	int risultato;

	risultato = ...
		
	return risultato;
}

I blocchi possono essere definiti all'interno di altri blocchi secondo una struttura ad albero:


int main() {
	int contatore = 0;

	while (contatore < 100) {
		int totale = 0;

		if (contatore < 50) {
			contatore += 2;
			totale ++;
		}

 		if (contatore >= 50) {
			contatore ++;
			totale++;
		}
			
		printf("valore di contatore = %d",contatore);
	}

	while (contatore > 0) {
		int totale = 0;

		if (contatore > 50) {
			contatore -= 2;
			totale--;

		}
		if (contatore <= 50) {
			contatore--;
			totale--;
		}
			
		printf("valore di contatore = %d",contatore);
	}

	return 0;
}

Questo piccolo programmino di esempio, dalla dubbia utilità pratica, serve ad illustrare il concetto di sopra. Si veda anche la figura 2.1.


Figura 2.1: albero dello scopo delle variabili

Il grafico illustra il concetto della località. Ogni rettangolo individua un blocco. Ogni blocco è collegato verso il basso con i blocchi contenuti e verso l'alto con il blocco in cui esso è contenuto. La struttura è necessariamente un albero perché un blocco non può essere contenuto in due blocchi differenti.
La visibilità delle variabili va verso il basso. Questo significa che una variabile definita in un blocco, esiste anche in tutti i blocchi in esso contenuti. E' il caso della variabile contatore che è unica in tutta la funzione ed è usata all'interno dei blocchi if.
L'intero programma costituisce la radice dell'albero, ovvero il primo nodo. Poiché ogni blocco deriva dalla radice una variabile ivi definita è una variabile globale.
Una variabile definita all'inizio della funzione, esiste anche nel blocco while, ma non è vero il contrario. E' il caso della variabile totale che è definita dentro il blocco while ma non al di fuori di esso.

Inoltre le due variabili totale sono definite in due blocchi diversi; anche se hanno lo stesso nome non interagiscono in quanto sono due variabili differenti, con scopo differente.
Nell'eventualità in cui due variabili con lo stesso nome sono definite in blocchi uno interno all'altro, dovrebbero esistere tutte e due, ma come è possibile accedere a tutte e due?. Si veda l'esempio:


int main() {
	int contatore = 0;
	int totale = 0;

	while (contatore < 100) {
		int totale = 0;

		if (contatore < 50) {
			contatore += 2;
			totale ++;
		}

 		if (contatore >= 50) {
			contatore ++;
			totale++;
		}
			
		printf("valore di contatore = %d",contatore);
	}
}

All'interno del blocco while è definita una nuova variabile totale; in queste situazioni la nuova variabile si dice che "nasconde" quella già esistente. nell'esempio, all'interno del blocco while è visibile solo la variabile totale ivi definita. Al di fuori del blocco while invece si ritorna a vedere la variabile precedente.
Questo meccanismo è ottenuto mediante lo stack delle variabili, di cui discuteremo in seguito quando affronteremo le strutture dati.

Variabili globali

Una variabile definita all'esterno di tutte le funzioni è chiamata variabile globale e come scopo ha l'intero programma. Queste variabili, poichè hanno la stessa vita del programma sono allocate in una zona di memoria fissa che viene richiesta quando il programma viene eseguito e viene rilasciata quando il programma termina.

Le variabili globali possono essere utili quando molte parti del programma utilizzano gli stessi dati.
Tuttavia l'uso delle variabili globali deve essere evitato il più possibile perché va contro i principi della programmazione strutturata. Ad esempio le funzioni che utilizzano delle variabili globali, non sono più dei moduli totalmente indipendenti dal resto del codice. Tali funzioni inoltre perdono di generalità; sono difficili da riutilizzare e da comprendere.
Inoltre poichè le variabili globali usano la memoria per tutto il tempo di esecuzione del programma e non solo quando servono, sono delle presenze ingombranti.

Parametri formali

Quando una funzione viene richiamata accetta dei parametri in ingresso. I valori di tali parametri vengono "ospitati" in delle variabili, chiamate parametri formali della funzione. Esse sono a tutti gli effetti delle variabili locali della funzione e possono essere lette e modificate. Riprenderemo in seguito questo discorso quando analizzeremo le funzioni.

Modificatori di accesso

Il C definisce due parole chiave, che controllano il modo in cui le variabili possono essere lette e modificate. Questi due qualificatori sono const e volatile. Esempio:


const double pigreco = 3.14;
volatile int secondi;

La parola chiave const crea una variabile del tipo specificato che è una costante, ovvero può essere letta e riferita, ma non modificata. Questa parola chiave è molto utile in fase di apprendimento della programmazione perchè fornisce dei limiti che aiutano a indirizzare il programmatore verso uno stile corretto e pulito. Se un puntatore viene dichiarato costante, punterà sempre nella stessa zona di memoria, evitando il pericolo di toccare zone di memoria errate.


const int massimo_valore = 10;
int contatore = 0;

while (contatore < massimo_valore)
	contatore++;

Nell'esempio di sopra, massimo_valore è una costante. Usare la costante al posto che direttamente il valore 10 è di notevole aiuto. Per prima cosa l'etichetta della costante se è significativa ci aiuta a capire qual'è il suo scopo: il contatore viene incrementato finché non raggiunge il valore della costante.
Inoltre se la costante viene usata in più punti del codice e deve essere modificata, cambiare una volta sola il valore è più veloce che cercare tutti i punti in cui è stata usato.

Il modificatore volatile invece informa il compilatore che il valore di una variabile può cambiare in modi non specificati del programma. Ad esempio il timer interno del computer viene aggiornato automaticamente dal clock a intervalli prestabiliti. Se a una variabile viene assegnato tale valore, il compilatore deve sapere che il suo contenuto può cambiare indipendentemente dal programma. Non faremo grande uso di questo modificatore.

Specificatori di memorizzazione

Il C consente l'uso di specificatori di memorizzazione: extern, static, register e auto. Questi specificatori indicano la modalità di memorizzazione della variabile. Esempio.


extern int valore;
static double numero_di_chiamate;
register int temporanea;

Poiché il C consente la compilazione separata di più moduli, può capitare che una variabile (o una funzione) usata in un modulo sia definita in un altro. In questo caso la parola chiave extern permette di indicare al compilatore di non preoccuparsi di cercare la dichiarazione, perché si trova da qualche altra parte. Naturalmente se in fase di Link la variabile non è stata dichiarata da nessuna parte verrà indicato un errore di riferimento.

Le variabili di tipo static sono delle variabili che assumono "artificialmente" una vita pari a quella del programma. Una variabile locale static definita dentro una funzione quindi non verrà più allocata e deallocata ogni volta che la funzione viene chiamata e poi termina. Essa conserverà sempre il suo valore. Queste variabili non si usano molto spesso, ma sono assolutamente ineguagliabili in fase di debug, quando si deve scoprire qualche errore nascosto dentro una funzione. Ad esempio si può creare una variabile static che conta il numero di volte che la funzione viene richiamata.

Un uso particolare del modificatore static è quello di creare una variabile globale static. Tale variabile è una variabile globale, ma solo all'interno del file in cui viene dichiarata. Questo è un utile espediente per limitare la "globalità" di una variabile a un insieme di funzioni.

Lo specificatore di memorizzazione di tipo register si applica solo a int e char e indica al compilatore di riservare una zona di memoria veloce (un registro) per tale variabile. Questo fa si che le operazioni su tale variabile sono svolte molto più velocemente rispetto alle altre variabili. Si deve fare un uso limitato di questo specificatore perché i registri della CPU sono in numero limitato e assegnarli alle variabili in maniera indiscriminata peggiora le prestazioni del programma piuttosto che migliorarle.

Lo specificatore auto è caduto in disuso. Prima serviva a dichiarare delle variabili locali. Adesso non c'è più bisogno di usarlo ma è stato mantenuto per questioni di compatibilità verso il basso con il suo predecessore (il linguaggio B).

Conversioni

Un argomento molto interessante su cui discutere è l'uso dei tipi. Abbiamo visti quanto siano pochi i tipi base del C ma quante siano le possibili combinazioni grazie a modificatori e specificatori di accesso e di memorizzazione. Ma ogni tipo è "logicamente" differente da ogni altro. In altri linguaggi una assegnazione tra tipi diversi è un errore non soltanto sintattico ma prima di tutto logico. Se C è una carattere e B e un intero, che cosa significa assegnare a B, il valore di C? Naturalmente per non smentire la libertà del nostro linguaggio, il C permette di fare questo genere di assegnamenti.

Se in una espressione ci sono variabili di tipo differente:


double lato = 2.5;
int area = 10;

int altezza = area / lato;	

ogni variabile dell'espressione viene convertita nel tipo che occupa più spazio (nell'esempio è double). Ma cosa vuol dire convertire un tipo in un altro? Solo i tipi predefiniti possono essere convertiti tra loro. La conversione è a tutti gli effetti un operatore anche se non esiste alcun simbolo che la identifica.

Le conversioni possono essere di due tipi, implicite, che sono quelle appena viste ed esplicite.

Conversioni esplicite o cast

In determinati casi è necessario forzare la conversione da un tipo ad un'altro. Supponiamo che una funzione restituisca la temperatura misurata in gradi con la precisione del double. Tuttavia questa temperatura interessa ai nostri scopi con una precisione minore, ad esempio un numero intero.


int gradi = misura_temperatura(Arizona);

Questa espressione genera un errore di compilazione. Il compilatore infatti nel controllo sintattico determina che si tenta di effettuare un assegnamento tra due tipi differenti, e questo è considerato un errore.
Si può effettuare la conversione cast:


int gradi = (int) misura_temperatura(Arizona);

La forma generale per l'operatore cast è (tipo) espressione.

A volte l'uso degli operatori cast è essenziale, ma in molti casi risulta evitabile. Ad esempio se disponessimo di una funzione che dato un double restituisce la parte intera otterremmo lo stesso risultato:


int gradi = parte_intera( misura_temperatura(Arizona) );

Oppure se vogliamo arrotondare il risultato al'intero più vicino possiamo usare:


int gradi = arrotonda( misura_temperatura(Arizona) );

Tutte le volte che riusciamo a evitare di inserire una conversione cast riusciamo a rendere il listato più comprensibile a chiunque lo legga, noi compresi. Per rendersene conto basta osservare le ultime tre righe di codice e leggere. Le ultime due sembrano sono auto esplicative, rispetto alla prima.

Nella prossima lezione vedremo le strutture principali del linguaggio e finalmente potremo scrivere qualche riga di codice funzionante.


Bibliografia


Testi consigliati per l'apprendimento

Questo articolo è stato scaricato dal Club di informatica
Pagina curata da Luca Sabatucci